package com.cooksys.film_rental.dao.impl;

import java.util.List;

import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import com.cooksys.film_rental.dao.FilmDao;
import com.cooksys.film_rental.model.Actor;
import com.cooksys.film_rental.model.Category;
import com.cooksys.film_rental.model.Film;
import com.cooksys.film_rental.model.FilmActor;
import com.cooksys.film_rental.model.FilmCategory;
import com.cooksys.film_rental.model.FilmText;
import com.cooksys.film_rental.model.Inventory;
import com.cooksys.film_rental.model.Language;
import com.cooksys.film_rental.model.Store;

@Repository("FilmDao")
@Transactional
public class FilmDaoImpl implements FilmDao {

	@Autowired(required = true)
	private SessionFactory sessionFactory;

	/**
	 * 
	 * @return the current session from the internal {@link SessionFactory}
	 */
	public Session currentSession() {
		return sessionFactory.getCurrentSession();
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<Film> searchFilmsByPartialTitle(String partialTitle) {
		Session session = currentSession();

		// split the search string into search terms
		String[] titleFragments = partialTitle.split("\\s");

		// 'select' statement of the hql query
		String hql = "from Film f where ";

		// generating the where clause
		for (int i = 0; i < titleFragments.length; i++) {

			// for each search term, add a segment of the where clause that
			// checks title column of the FILM table for matches
			hql = hql + "f.title like :titleFragment" + i;

			if (i < titleFragments.length - 1) {

				// concatenate these where clause segments with an 'or' operator
				hql = hql + " or ";

			}

		}

		Query query = session.createQuery(hql);

		for (int i = 0; i < titleFragments.length; i++) {

			// parameterize the query with the search terms
			query.setString("titleFragment" + i, "%" + titleFragments[i] + "%");

		}

		return query.list();
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<Film> searchFilmsByLanguage(Language language) {
		Session session = currentSession();

		// selects the film objects associated with a given language
		String hql = "from Film f where f.languageByLanguageId = :language";
		Query query = session.createQuery(hql);
		query.setEntity("language", language);

		return query.list();

	}

	@SuppressWarnings("unchecked")
	@Override
	public List<Film> searchFilmsByOriginalLanguage(Language originalLanguage) {
		Session session = currentSession();

		// selects the film objects associated with a given original language
		String hql = "from Film f where f.languageByOriginalLanguageId = :originalLanguage";
		Query query = session.createQuery(hql);
		query.setEntity("originalLanguage", originalLanguage);

		return query.list();
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<Film> searchFilmsByActor(Actor actor) {
		Session session = currentSession();

		// selects the film objects associated with a given actor
		String hql = "select f.film from FilmActor f where f.actor = :actor";
		Query query = session.createQuery(hql);
		query.setEntity("actor", actor);

		return query.list();
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<Film> searchFilmsByCategory(Category category) {
		Session session = currentSession();

		// selects the film objects associated with a given category
		String hql = "select f.film from FilmCategory f where f.category = :category";
		Query query = session.createQuery(hql);
		query.setEntity("category", category);

		return query.list();
	}

	@Override
	public Film getFilmByFilmId(Short filmId) {
		Session session = currentSession();

		// returns a record from the FILM table as a Film object based on the
		// given filmId, the table's primary key
		String hql = "from Film f where f.filmId = :filmId";

		Query query = session.createQuery(hql);
		query.setShort("filmId", filmId);

		return (Film) query.list().get(0);
	}

	@Override
	public Film getFilmByInventory(Inventory inventory) {
		Session session = currentSession();

		// selects the film object associated with a given inventory
		String hql = "select i.film from Inventory i where i = :inventory";
		Query query = session.createQuery(hql);
		query.setEntity("inventory", inventory);

		return (Film) query.list().get(0);
	}

	@Override
	public Film getFilmByFilmActor(FilmActor filmActor) {
		Session session = currentSession();

		// selects the film object associated with a given filmActor
		String hql = "select f.film from FilmActor f where f = :filmActor";
		Query query = session.createQuery(hql);
		query.setEntity("filmActor", filmActor);

		return (Film) query.list().get(0);
	}

	@Override
	public Film getFilmByFilmCategory(FilmCategory filmCategory) {
		Session session = currentSession();

		// selects the film object associated with a given filmCategory
		String hql = "select f.film from FilmCategory f where f = :filmCategory";
		Query query = session.createQuery(hql);
		query.setEntity("filmCategory", filmCategory);

		return (Film) query.list().get(0);
	}

	@Override
	public long getNumCopiesAvailable(Film film) {
		Session session = currentSession();

		// selects the count of inventory objects associated with the given film
		// where the rental records associated with the inventory objects do not
		// contain a null value for the return_date column
		String hql = "select count(*) from Inventory i where i.film = :film and not exists ( select r.rentalId from Rental r where r.returnDate is null and r.inventory.inventoryId = i.inventoryId)";

		Query query = session.createQuery(hql);
		query.setEntity("film", film);

		return (Long) query.list().get(0);
	}

	@Override
	public long getNumCopiesCheckedOut(Film film) {
		Session session = currentSession();

		// Inverted select statement from getNumCopiesAvailable(Filmm film)
		String hql = "select count(*) from Inventory i where i.film = :film and exists ( select r.rentalId from Rental r where r.returnDate is null and r.inventory.inventoryId = i.inventoryId)";

		Query query = session.createQuery(hql);
		query.setEntity("film", film);

		return (Long) query.list().get(0);
	}

	@Override
	public FilmText getFilmTextByFilmId(Short filmId) {
		Session session = currentSession();

		// returns a record from the FILM_TEXT table as a FilmText object based
		// on the given filmId, the table's primary key
		String hql = "from FilmText f where f.filmId = :filmId";
		Query query = session.createQuery(hql);
		query.setShort("filmId", filmId);

		return (FilmText) query.list().get(0);
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<String> getAllRatings() {
		Session session = currentSession();

		String hql = "select distinct f.rating from Film f";
		Query query = session.createQuery(hql);

		return query.list();
	}

	@Override
	public short getMaxRuntime() {
		Session session = currentSession();

		String hql = "select f.length from Film f order by f.length desc";
		Query query = session.createQuery(hql);

		return (Short) query.list().get(0);
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<Film> advancedSearch(String partialTitle,
			Category categoryFilter, Actor actorFilter,
			Language languageFilter, Language originalLanguageFilter,
			String ratingFilter, String runtimeFilter) {
		Session session = currentSession();

		// foundation of the hql statement
		String hql = "from Film f where ";

		// add title matching

		// create title where clause
		String hqlTitle = "";

		// split the search string into search terms
		String[] titleFragments = partialTitle.split("\\s");

		// generating the where clause
		for (int i = 0; i < titleFragments.length; i++) {

			// for each search term, add a segment of the where clause that
			// checks title column of the FILM table for matches
			hqlTitle = hqlTitle + "f.title like :titleFragment" + i;

			if (i < titleFragments.length - 1) {

				// concatenate these where clause segments with an 'or' operator
				hqlTitle = hqlTitle + " or ";

			}

		}

		// add where clause to query string
		hql = hql + "(" + hqlTitle + ")";

		// add category filter
		if (categoryFilter != null) {

			String hqlCategory = "exists ( select fC.id from FilmCategory fC where fC.film = f and fC.category = :category )";

			// add where clause to query string
			hql = hql + " and (" + hqlCategory + ")";

		}

		// add actor filter
		if (actorFilter != null) {

			String hqlActor = "exists ( select fA.id from FilmActor fA where fA.film = f and fA.actor = :actor )";

			// add where clause to query string
			hql = hql + " and (" + hqlActor + ")";

		}

		// add language filter
		if (languageFilter != null) {

			String hqlLanguage = "f.languageByLanguageId = :language";

			// add where clause to query string
			hql = hql + " and (" + hqlLanguage + ")";

		}

		// add original language filter
		if (originalLanguageFilter != null) {

			String hqlOriginalLanguage = "f.languageByOriginalLanguageId = :originalLanguage";

			// add where clause to query string
			hql = hql + " and (" + hqlOriginalLanguage + ")";

		}

		// add rating filter
		if (!ratingFilter.equalsIgnoreCase("ALL")) {

			String hqlRating = "f.rating = :rating";

			// add where clause to query string
			hql = hql + " and (" + hqlRating + ")";

		}

		// add runtime filter
		if (!runtimeFilter.equalsIgnoreCase("ALL")) {

			String hqlRuntime = "f.length <= :runtime";

			// add where clause to query string
			hql = hql + " and (" + hqlRuntime + ")";

		}

		// create the query
		Query query = session.createQuery(hql);

		// set the query parameters

		// set title search parameters
		for (int i = 0; i < titleFragments.length; i++) {

			// parameterize the query with the search terms
			query.setString("titleFragment" + i, "%" + titleFragments[i] + "%");

		}

		// set the category
		if (categoryFilter != null) {
			query.setEntity("category", categoryFilter);
		}

		// set the actor
		if (actorFilter != null) {
			query.setEntity("actor", actorFilter);
		}

		// set the language
		if (languageFilter != null) {
			query.setEntity("language", languageFilter);
		}

		// set the original language
		if (originalLanguageFilter != null) {
			query.setEntity("originalLanguage", originalLanguageFilter);
		}

		// set the rating
		if (!ratingFilter.equalsIgnoreCase("ALL")) {
			query.setString("rating", ratingFilter);
		}

		// set the runtime
		if (!runtimeFilter.equalsIgnoreCase("ALL")) {
			Short runtimeAsShort = Short.valueOf(runtimeFilter);
			query.setShort("runtime", runtimeAsShort);
		}

		// return the results of the query
		return query.list();

	}

	@Override
	public boolean getFilmAvailabilityByStore(Film film, Store store) {
		Session session = currentSession();

		// selects the count of inventory objects associated with the given film
		// where the rental records associated with the inventory objects do not
		// contain a null value for the return_date column
		String hql = "select count(*) from Inventory i where i.film = :film and i.store = :store and not exists ( select r.rentalId from Rental r where r.returnDate is null and r.inventory.inventoryId = i.inventoryId)";

		Query query = session.createQuery(hql);
		query.setEntity("film", film);
		query.setEntity("store", store);

		Long availableCopies = (Long) query.list().get(0);
		return availableCopies > 0;
	}

}
